﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Iit.ITBI.MDSManager.Microsoft.MasterDataServices;
using System.Data;
using System.Data.SqlClient;


namespace Iit.ITBI.MDSManager
{
    /// <summary>
    /// Perform the needed actions to publish\ replicate the master entity to a slave entity in a MasterData Services instance.
    /// The Replicator make sure that the slave entity has the same attributes and the same data as the master entity.
    /// </summary>
    public class MasterEntityReplicator
    {
        private ServiceClient mdsServiceProxy;
        private Uri mdsServiceAddress;
        private MetadataCache metadataCache;
        private int errorCounter;
        private string mdsdbConnectionString;
        private International international;
        private OperationResult operationResult;

        public Uri MDSServiceAddress
        {
            get { return mdsServiceAddress; }            
        }
        public string MDSDBConnectionString
        {
            get { return mdsdbConnectionString; }
        }
        
        
        #region CTOR
        /// <summary>
        /// Initializes a new instance of the <see cref="MasterEntityReplicator"/> class.
        /// </summary>
        /// <param name="mdsServiceAddress">The MDS web service address.</param>
        /// <param name="mdsdbConnectionString">The MDS database connection string.</param>
        public MasterEntityReplicator(Uri mdsServiceAddress, string mdsdbConnectionString)
        {
            this.mdsServiceAddress = mdsServiceAddress;

            this.mdsServiceProxy = this.CreateMdsProxy();

            this.mdsdbConnectionString = mdsdbConnectionString;

            this.metadataCache = new MetadataCache(mdsdbConnectionString);

            this.errorCounter = 0;

            this.international = new International();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MasterEntityReplicator"/> class.
        /// </summary>
        public MasterEntityReplicator()
        {
            this.mdsServiceAddress = new Uri(Properties.Settings.Default.MasterDataServicesServiceAddress);

            this.mdsServiceProxy = this.CreateMdsProxy();

            this.mdsdbConnectionString = Properties.Settings.Default.MasterDataServicesDatabaseConString;

            this.metadataCache = new MetadataCache(mdsdbConnectionString);

            this.errorCounter = 0;

            this.international = new International();
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Publishes master entity schema and data to all subscribing slave entities.
        /// </summary>
        /// <exception cref="NotSupportedMappingStateException"></exception>
        /// <exception cref="UnknownMappingStateException"></exception>
        /// <exception cref="System.NotSupportedException"></exception>
        public void Replicate()
        {

            try
            {
                /// refresh the local metadata from the MDS database
                metadataCache.RefreshLocalCache();


                ///iterate the list of master to slave Entity mappings in the metadata cache
                 foreach (MasterToSlaveEntityMapping masterToSlaveEntityMapping in metadataCache.MasterToSlaveMappings)
                {
                    //check that the mapping is not using a slave entity as the master, if so we rasie a warning event, and we move on to the next item in the list.
                    if (masterToSlaveEntityMapping.MasterEntity.Name.IndexOf("_Slave") != -1)
                    {
                        InvokeWarningEvent(string.Format("The mapping is using a slave entity \"{0}\" from model \"{1}\"as a master entity. This is not allowed, and therefore the mapping is ignored"
                                            , masterToSlaveEntityMapping.MasterEntity.Name
                                            , masterToSlaveEntityMapping.MasterEntity.Model.Name));
                        continue;
                        
                    }

                    InvokeInfoEvent(string.Format("Start process the mapping between master entity \"{0}\" in model \"{1}\" and slave entity \"{2}\" in model \"{3}\". Mapping code: {4}; Mapping State {5}"
                                            , masterToSlaveEntityMapping.MasterEntity.Name
                                            , masterToSlaveEntityMapping.MasterEntity.Model.Name
                                            , masterToSlaveEntityMapping.SlaveEntity.Name
                                            , masterToSlaveEntityMapping.SlaveEntity.Model.Name
                                            , masterToSlaveEntityMapping.MappingCode.ToString()
                                            , masterToSlaveEntityMapping.MappingState.ToString()));
                    
                    ///enrich the cache with server side identifiers
                    EnrichMetadataCache(masterToSlaveEntityMapping);

                    ///first we handle unknown and unsupported mapping states
                    if (masterToSlaveEntityMapping.MappingState == MDSMappingState.NotSupported)
                    {
                        throw new NotSupportedMappingStateException(string.Format("The mapping between the Entity \"{0}\" from model \"{1}\" to model \"{2}\" returned an unsupported MappingState"
                                , masterToSlaveEntityMapping.MasterEntity.Name, masterToSlaveEntityMapping.MasterEntity.Model.Name, masterToSlaveEntityMapping.SlaveEntity.Name)
                                , masterToSlaveEntityMapping);
                    }

                    if (masterToSlaveEntityMapping.MappingState == MDSMappingState.Unknown)
                    {
                        throw new UnknownMappingStateException(string.Format("The mapping between the Entity \"{0}\" from model \"{1}\" to model \"{2}\" returned an unknown MappingState"
                                , masterToSlaveEntityMapping.MasterEntity.Name, masterToSlaveEntityMapping.MasterEntity.Model.Name, masterToSlaveEntityMapping.SlaveEntity.Name)
                                , masterToSlaveEntityMapping);
                    }

                    ///second we check if the mapping is deleted 
                    ///if so we delete the slave Entity, and continue to the next item in the MasterToSlaveMappings collection
                    if (masterToSlaveEntityMapping.MappingState == MDSMappingState.Deleted && masterToSlaveEntityMapping.SlaveEntity.Exists == true)
                    {
                        InvokeInfoEvent(string.Format("Deleting Slave Entity \"{0}\" from model \"{1}\".", masterToSlaveEntityMapping.SlaveEntity.Name, masterToSlaveEntityMapping.SlaveEntity.Model.Name));

                        DeleteSlaveEntity(masterToSlaveEntityMapping);

                        InvokeInfoEvent(string.Format("Deleting the mapping permenantly from MDS. Mapping code is: \"{0}\" entity is \"{1}\" from model \"{2}\" to model \"{3}\"."
                                                            ,masterToSlaveEntityMapping.MappingCode.ToString()
                                                            ,masterToSlaveEntityMapping.SlaveEntity.Name
                                                            ,masterToSlaveEntityMapping.MasterEntity.Model.Name
                                                            ,masterToSlaveEntityMapping.SlaveEntity.Model.Name));

                        MDSSqlWrapper.GlobalInstance.PermenantDeleteMapping(masterToSlaveEntityMapping.MappingCode.ToString());

                        ///force the loop to continue with the next item
                        continue;
                    }

                    ///lastly we check if the mapping is valid
                    if (masterToSlaveEntityMapping.MappingState == MDSMappingState.Valid)
                    {

                        ///check if the slave Entity exits, if not create it
                        ///when creating masterAttributeMetadata new Entity, all attributes are aligned by default
                        if (masterToSlaveEntityMapping.SlaveEntity.Exists != true)
                        {
                            InvokeInfoEvent(string.Format("Create new slave entity by copying the entity \"{0}\" from model \"{1}\" to model \"{2}\""
                                                , masterToSlaveEntityMapping.MasterEntity.Name
                                                , masterToSlaveEntityMapping.MasterEntity.Model.Name
                                                , masterToSlaveEntityMapping.SlaveEntity.Name));

                            CreateNewSlaveEntity(masterToSlaveEntityMapping);
                        }
                        
                        ///if the slave entity exists, then we need to align the attributes between the master and the slave entity
                        else
                        {
                            InvokeInfoEvent(string.Format("Align attributes between master entity \"{0}\" in model \"{1}\" and slave entity \"{2}\" in model \"{3}\"."
                                            , masterToSlaveEntityMapping.MasterEntity.Name
                                            , masterToSlaveEntityMapping.MasterEntity.Model.Name
                                            , masterToSlaveEntityMapping.SlaveEntity.Name
                                            , masterToSlaveEntityMapping.SlaveEntity.Model.Name));
                            
                            AlignAttributes(masterToSlaveEntityMapping);
                        }

                        ///now that the slave entity schema is fully aligned with the master entity we replicate the members from the master to the slave
                        ReplicateEntityMembers(masterToSlaveEntityMapping, Properties.Settings.Default.GetMembersMaxBufferSize);

                        ///after replicating new and changed we remove any members that only exits in the slave entity
                        //we verify that the table for the slave is not null, as this is needed for the deletion part.
                        
                        //RemoveDeletedMembers(masterToSlaveEntityMapping, Properties.Settings.Default.GetMembersMaxBufferSize);
                        if (masterToSlaveEntityMapping.SlaveEntity.EntityTableName != null)
                        {
                            RemoveDeletedMembers(masterToSlaveEntityMapping);
                        }
                        
                    }

                }

                ///when finishing a publish look without errors we reset the error counter
                errorCounter = 0;

            }
            catch (InvalidMetadataCacheException e)
            {
                StringBuilder message = new StringBuilder();

                message.AppendLine("The metadata local cache is not matching current metadata on the server.");
                message.AppendLine("This can happen when changes occurs on the server during a publish procedure.");
                message.AppendLine("Metadata local cache details are: ");
                message.AppendLine(e.MasterToSlaveEntityMapping.ToString());

                ///if the number of errors had passed the max accepted this exception should propagate to the application
                ///if not we just add one to the counter.
                ///We do this as this type of exception is not fatal, and the next publish should be able to solve it.
                if (errorCounter > Properties.Settings.Default.ConsequentErrorsAllowed)
                {
                    message.AppendLine(string.Format("Propagating the error as the number of consequent errors ({0}) exceeded the allowed number.", errorCounter));

                    InvokeErrorEvent(message.ToString(), e);

                    throw;
                }
                else
                {
                    message.AppendLine(string.Format("Number of consequent errors is ({0}).", errorCounter));
                    InvokeWarningEvent(message.ToString(), e);

                    errorCounter++;
                }
            }

            catch (NotSupportedMappingStateException e)
            {
                InvokeErrorEvent(e.Message, e);

                throw;
            }

            catch (UnknownMappingStateException e)
            {
                InvokeErrorEvent(e.Message, e);

                throw;
            }

            catch (InvalidSyncStateException e)
            {
                InvokeErrorEvent(e.Message, e);

                throw;
            }
            catch (UnknownSyncStateException e)
            {
                InvokeErrorEvent(e.Message, e);

                throw;
            }
            catch (NotSupportedQueryVersionException e)
            {
                InvokeErrorEvent(e.Message, e);

                throw;
            }
            catch (InvalidMetadataException e)
            {
                InvokeErrorEvent(e.Message, e);

                throw;
            }
            catch (MDSMetadataCommandException e)
            {
                InvokeErrorEvent(e.Message, e);

                throw;
            }
            catch (Exception e)
            {
                {
                    InvokeErrorEvent("Unhandled Exception", e);

                    throw;
                }
            }
            
        }
        #endregion

        #region Private methods

        /// <summary>
        /// Creates proxy service against the Masterdata Services WebService.
        /// </summary>
        /// <returns>A ServiceClient instance.</returns>
        private ServiceClient CreateMdsProxy()
        {
            // create an endpoint address using the URL  
            System.ServiceModel.EndpointAddress endptAddress = new System.ServiceModel.EndpointAddress(mdsServiceAddress);

            // create and configure the WS Http binding  
            System.ServiceModel.WSHttpBinding wsBinding = new System.ServiceModel.WSHttpBinding();

            wsBinding.MaxBufferPoolSize = 2147483647;

            wsBinding.MaxReceivedMessageSize = 2147483647;

            // create and return the client proxy  
            return new ServiceClient(wsBinding, endptAddress);
        }

        /// <summary>
        /// Validates the entity, which means to run any business rule against any member that is not yet validated.
        /// </summary>
        /// <param name="MasterToSlaveEntityMapping">The master to slave entity mapping.</param>
        /// <param name="Members">The number of validated members.</param>
        /// <returns></returns>
        private int ValidateEntityMembers(MasterToSlaveEntityMapping MasterToSlaveEntityMapping, MemberIdentifier[] Members)
        {
            try
            {

                ValidationIssue[] validationIssues;
                ValidationProcessResult validationProcessResult = new ValidationProcessResult();
                ValidationProcessCriteria validationProcessCriteria = new ValidationProcessCriteria();
                ValidationProcessOptions validationProcessOptions = new ValidationProcessOptions();

                validationProcessCriteria.EntityId = MasterToSlaveEntityMapping.SlaveEntity.Identifier;
                validationProcessCriteria.ModelId = MasterToSlaveEntityMapping.SlaveEntity.Model.Identifier;
                validationProcessCriteria.VersionId = MasterToSlaveEntityMapping.SlaveEntity.Model.VersionID;
                validationProcessCriteria.Members = Members;

                validationProcessOptions.ReturnValidationResults = true;
                validationProcessOptions.CommitVersion = false;

                operationResult = mdsServiceProxy.ValidationProcess(international, validationProcessCriteria, validationProcessOptions, out validationIssues, out validationProcessResult);

                ProcessResultForErrors(operationResult, validationProcessResult, validationIssues);

                return validationProcessResult.MembersValidatedCount;

            }
            catch (MemberValidationIssuesException e)
            {
                ///this type of exception is not fatal, so we treat it as a warning

                ///if the mapping is not assigned in the exception we add it
                if (e.MasterToSlaveEntityMapping == null)
                {
                    e.MasterToSlaveEntityMapping = MasterToSlaveEntityMapping;
                }

                StringBuilder message = new StringBuilder();

                message.AppendLine(string.Format("Validation issues occurred with one or more members of the entity \"{0}\" in model \"{1}\""
                                                    , MasterToSlaveEntityMapping.SlaveEntity.Name
                                                    , MasterToSlaveEntityMapping.SlaveEntity.Model.Name));
                message.AppendLine(string.Format("Showing the first 10 issues out of {0}", e.ValidationIssues.Length.ToString()));

                for (int i = 0; i < e.ValidationIssues.Length || i < 10; i++)
                {
                    message.AppendLine(string.Format("Issue #{0}: {1}", i.ToString(), e.ValidationIssues[i].Description));
                }

                InvokeWarningEvent(message.ToString(), e);

            }
            catch (MDSException e)
            {
                if (e.MasterToSlaveEntityMapping == null)
                {
                    e.MasterToSlaveEntityMapping = MasterToSlaveEntityMapping;
                }

                if (!e.Data.Contains("CustomCallStack"))
                {
                    e.Data.Add("CustomCallStack", "at ValidateEntityMembers()");
                }
                else
                {
                    e.Data["CustomCallStack"] = "at ValidateEntityMembers()\r\n" + e.Data["CustomCallStack"].ToString();
                }

                throw;
            }
            catch (Exception e)
            {

                if (!e.Data.Contains("CustomCallStack"))
                {
                    e.Data.Add("CustomCallStack", "at ValidateEntityMembers()");
                }
                else
                {
                    e.Data["CustomCallStack"] = "at ValidateEntityMembers()\r\n" + e.Data["CustomCallStack"].ToString();
                }

                throw;
            }
            
            
            return -1;
            
        }

        /// <summary>
        /// Replicate the entity members (that is data rows) from the master to the slave entity.
        /// New members are created, and members that have changes are updated.
        /// The method will fetch the list of members from the master entity, in a iteration pattern using ResponseBufferSize to
        /// define how many rows to retrieve in each iteration.
        /// </summary>
        /// <param name="masterToSlaveEntityMapping">The master to slave entity mapping.</param>
        /// <param name="ResponseBufferSize">The number of members to return in each iteration.</param>
        private void ReplicateEntityMembers(MasterToSlaveEntityMapping MasterToSlaveEntityMapping, int ResponseBufferSize)
        {

            try
            {

                string modifiedAttributesCodes = "";
                
                ///check to see if the slave entity exists, if it exists, then we only want to replicated changed data between the two tables
                if (MasterToSlaveEntityMapping.SlaveEntity.Exists == true)
                {
                    ///get the actual list of rows that are changed and needs to be replicated                
                    modifiedAttributesCodes = MDSSqlWrapper.GlobalInstance.GetModifiedAttributesCodes
                                                                                        (MasterToSlaveEntityMapping.MasterEntity.EntityTableName
                                                                                        , MasterToSlaveEntityMapping.SlaveEntity.EntityTableName);
                    ///if there are changes to replicate, we exit the method
                    if (modifiedAttributesCodes == null)
                    {

                        InvokeInfoEvent(string.Format("No changes to replicate from master entity \"{0}\" in model \"{1}\" to slave entity \"{2}\" in model \"{3}\"."
                                                , MasterToSlaveEntityMapping.MasterEntity.Name
                                                , MasterToSlaveEntityMapping.MasterEntity.Model.Name
                                                , MasterToSlaveEntityMapping.SlaveEntity.Name
                                                , MasterToSlaveEntityMapping.SlaveEntity.Model.Name));

                        return;
                    }
                }
                EntityMembers slaveEntityMembers = new EntityMembers();

                slaveEntityMembers.EntityId = MasterToSlaveEntityMapping.SlaveEntity.Identifier;
                slaveEntityMembers.ModelId = MasterToSlaveEntityMapping.SlaveEntity.Model.Identifier;
                slaveEntityMembers.VersionId = MasterToSlaveEntityMapping.SlaveEntity.Model.VersionID;
                slaveEntityMembers.MemberType = MemberType.Leaf;

                EntityMembersGetCriteria membersGetCriteria = new EntityMembersGetCriteria();
                membersGetCriteria.EntityId = MasterToSlaveEntityMapping.MasterEntity.Identifier;
                membersGetCriteria.ModelId = MasterToSlaveEntityMapping.MasterEntity.Model.Identifier;
                membersGetCriteria.VersionId = MasterToSlaveEntityMapping.MasterEntity.Model.VersionID;
                membersGetCriteria.AttributeGroupId = new Identifier();
                membersGetCriteria.PageSize = ResponseBufferSize;
                membersGetCriteria.DisplayType = new DisplayType?(DisplayType.NameCode);
                membersGetCriteria.MemberReturnOption = MemberReturnOption.DataAndCounts;
                membersGetCriteria.MemberType = MemberType.Leaf;

                if (modifiedAttributesCodes != "")
                {
                    membersGetCriteria.SearchTerm = String.Format(" [Code] IN ({0}) ", modifiedAttributesCodes);
                }
                EntityMembersInformation entityMembersInformation = new EntityMembersInformation();
                MemberIdentifier[] createdMembers;

                int currentPageIndex = 0;
                int totalMemberCount = 0;

                do
                {
                    membersGetCriteria.PageNumber = currentPageIndex;

                    slaveEntityMembers.Members = mdsServiceProxy.EntityMembersGet(international, membersGetCriteria, out entityMembersInformation, out operationResult).Members;

                    ProcessResultForErrors(operationResult);

                    if (slaveEntityMembers.Members.Length > 0)
                    {
                        ///this is a bug workaround
                        ///the EntityMembersMerge is able to return the idenfiers fo the newly created members. We need these identifiers to later use them in the validation process.
                        ///However, when using the option of retriving the created members, the merge does not work the way it should, and data is not merged correctly
                        ///To solve this, we call the EntityMembersMerge twice, once with the ReturnCreatedIdentifires sat to true and capturing the identifiers of the
                        ///newly created rows, and once with the ReturnCreatedIdentifires sat to false, which is the actual call that is used for the merge of the data
                        ///we then process the result for errors, and lastly we call the ValidateEntityMemebers using the identifiers array.

                        createdMembers = mdsServiceProxy.EntityMembersMerge(international, slaveEntityMembers, true, out operationResult);
                        
                        mdsServiceProxy.EntityMembersMerge(international, slaveEntityMembers, false, out operationResult);

                        ProcessResultForErrors(operationResult);

                        ValidateEntityMembers(MasterToSlaveEntityMapping, createdMembers);

                        totalMemberCount += slaveEntityMembers.Members.Length;
                    }

                    currentPageIndex++;                     

                }
                while (slaveEntityMembers.Members.Length == ResponseBufferSize);

                InvokeInfoEvent(string.Format("Replicate {0} entity members (data) from master entity \"{1}\" in model \"{2}\" and slave entity \"{3}\" in model \"{4}\"."
                                            , totalMemberCount.ToString()
                                            , MasterToSlaveEntityMapping.MasterEntity.Name
                                            , MasterToSlaveEntityMapping.MasterEntity.Model.Name
                                            , MasterToSlaveEntityMapping.SlaveEntity.Name
                                            , MasterToSlaveEntityMapping.SlaveEntity.Model.Name));

            }
            catch (MDSException e)
            {
                if (e.MasterToSlaveEntityMapping == null)
                {
                    e.MasterToSlaveEntityMapping = MasterToSlaveEntityMapping;
                }

                if (!e.Data.Contains("CustomCallStack"))
                {
                    e.Data.Add("CustomCallStack", "at ReplicateEntityMembers()");
                }
                else
                {
                    e.Data["CustomCallStack"] = "at ReplicateEntityMembers()\r\n" + e.Data["CustomCallStack"].ToString();
                }

                throw;
            }
            catch (Exception e)
            {
                if (!e.Data.Contains("CustomCallStack"))
                {
                    e.Data.Add("CustomCallStack", "at ReplicateEntityMembers()");
                }
                else
                {
                    e.Data["CustomCallStack"] = "at ReplicateEntityMembers()\r\n" + e.Data["CustomCallStack"].ToString();
                }


                throw;
            }
            
        }


        /// <summary>
        /// Removes members in the slave entity, that cannot be found in the master entity.
        /// The method will use the staging mechanism of the MDS, so we will load the missing codes to the staging table
        /// using the ImportType 6 and then process the staging table.
        /// </summary>
        /// <param name="MasterToSlaveEntityMapping">The master to slave entity mapping.</param>        
        private void RemoveDeletedMembers(MasterToSlaveEntityMapping MasterToSlaveEntityMapping)
        {

            operationResult =  MDSSqlWrapper.GlobalInstance.DeleteMembersFromSlaveEntity
                                    (MasterToSlaveEntityMapping.MasterEntity.EntityTableName
                                    , MasterToSlaveEntityMapping.SlaveEntity.EntityTableName
                                    , MasterToSlaveEntityMapping.SlaveEntity.Model.VersionID.Name
                                    ,MasterToSlaveEntityMapping.SlaveEntity.StagingBase);

            ProcessResultForErrors(operationResult);
        }

        /// <summary>
        /// Removes members in the slave entity, that cannot be found in the master entity.
        /// The method will fetch the members from the slave entity, using the collection MasterToSlaveEntityMapping.MembersMissingInMasterCodes as the filter
        /// That way we get a list of entity members, which then will get deleted from the slave entity.
        /// </summary>
        /// <param name="MasterToSlaveEntityMapping">The master to slave entity mapping.</param>
        /// <param name="ResponseBufferSize">Size of the response buffer.</param>
        private void RemoveDeletedMembers(MasterToSlaveEntityMapping MasterToSlaveEntityMapping, int ResponseBufferSize)
        {

            #region legacy code
            if (MasterToSlaveEntityMapping.MembersMissingInMasterCodes.Count() > 0)
            {

                try
                {
                    EntityMembers slaveEntityMembersToDelete = new EntityMembers();

                    slaveEntityMembersToDelete.EntityId = MasterToSlaveEntityMapping.SlaveEntity.Identifier;
                    slaveEntityMembersToDelete.ModelId = MasterToSlaveEntityMapping.SlaveEntity.Model.Identifier;
                    slaveEntityMembersToDelete.VersionId = MasterToSlaveEntityMapping.SlaveEntity.Model.VersionID;
                    slaveEntityMembersToDelete.MemberType = MemberType.Leaf;

                    mdsServiceProxy.EntityMembersDelete(international, slaveEntityMembersToDelete);

                    EntityMembersGetCriteria membersGetCriteria = new EntityMembersGetCriteria();
                    membersGetCriteria.EntityId = MasterToSlaveEntityMapping.SlaveEntity.Identifier;
                    membersGetCriteria.ModelId = MasterToSlaveEntityMapping.SlaveEntity.Model.Identifier;
                    membersGetCriteria.VersionId = MasterToSlaveEntityMapping.SlaveEntity.Model.VersionID;
                    membersGetCriteria.PageSize = ResponseBufferSize;
                    membersGetCriteria.AttributeGroupId = new Identifier();
                    membersGetCriteria.DisplayType = new DisplayType?(DisplayType.NameCode);
                    membersGetCriteria.MemberReturnOption = MemberReturnOption.DataAndCounts;
                    membersGetCriteria.MemberType = MemberType.Leaf;

                    membersGetCriteria.SearchTerm = string.Format("Code in ({0})", string.Join(",", MasterToSlaveEntityMapping.MembersMissingInMasterCodes.ToArray()));

                    EntityMembersInformation entityMembersInformation = new EntityMembersInformation();

                    int currentPageIndex = 0;
                    int totalMemberCount = 0;

                    do
                    {
                        membersGetCriteria.PageNumber = currentPageIndex;

                        slaveEntityMembersToDelete.Members = mdsServiceProxy.EntityMembersGet(international, membersGetCriteria, out entityMembersInformation, out operationResult).Members;

                        ProcessResultForErrors(operationResult);

                        if (slaveEntityMembersToDelete.Members.Length > 0)
                        {
                            operationResult = mdsServiceProxy.EntityMembersDelete(international, slaveEntityMembersToDelete);

                            ProcessResultForErrors(operationResult);

                            totalMemberCount += slaveEntityMembersToDelete.Members.Length;
                        }

                        currentPageIndex++;

                    }
                    while (slaveEntityMembersToDelete.Members.Length == ResponseBufferSize);

                    InvokeInfoEvent(string.Format("Delete {0} entity members (data) from slave entity \"{1}\" in model \"{2}\" as there are no longer to find in master entity \"{3}\" in model \"{4}\"."
                                                , totalMemberCount.ToString()
                                                , MasterToSlaveEntityMapping.SlaveEntity.Name
                                                , MasterToSlaveEntityMapping.SlaveEntity.Model.Name
                                                , MasterToSlaveEntityMapping.MasterEntity.Name
                                                , MasterToSlaveEntityMapping.MasterEntity.Model.Name));

                }
                catch (MDSException e)
                {
                    if (e.MasterToSlaveEntityMapping == null)
                    {
                        e.MasterToSlaveEntityMapping = MasterToSlaveEntityMapping;
                    }

                    if (!e.Data.Contains("CustomCallStack"))
                    {
                        e.Data.Add("CustomCallStack", "at RemoveDeletedMembers()");
                    }
                    else
                    {
                        e.Data["CustomCallStack"] = "at RemoveDeletedMembers()\r\n" + e.Data["CustomCallStack"].ToString();
                    }

                    throw;
                }
                catch (Exception e)
                {
                    if (!e.Data.Contains("CustomCallStack"))
                    {
                        e.Data.Add("CustomCallStack", "at RemoveDeletedMembers()");
                    }
                    else
                    {
                        e.Data["CustomCallStack"] = "at RemoveDeletedMembers()\r\n" + e.Data["CustomCallStack"].ToString();
                    }


                    throw;
                }

            }
            #endregion

        }

        /// <summary>
        /// Aligns the attributes of two existing entities.
        /// The alignment happens based on the local metadata cache.
        /// <list type="">
        /// <item>Newly created attributes on the master side are add to the slave.</item>
        /// <item>Attributes on the master side that were removed, are deleted from the slave.</item>
        /// <item>Different attribute definitions between the slave and the master are treated as delete and add.</item>
        /// </list>
        /// </summary>
        /// <param name="masterToSlaveEntityMapping">The local metadata cache in for of a master to slave mapping object.</param>
        private void AlignAttributes(MasterToSlaveEntityMapping MasterToSlaveEntityMapping)
        {
            


            Model slaveModel = GetModel(MasterToSlaveEntityMapping.SlaveEntity.Model.Name);

            Metadata masterEntity = GetEntityMetadata(MasterToSlaveEntityMapping.MasterEntity.Identifier);
            Metadata slaveEntity = GetEntityMetadata(MasterToSlaveEntityMapping.SlaveEntity.Identifier);
            

            ///create metadata objects to hold the current attributes that exists on the MDS Server
            Metadata masterEntityAttributesDetails = GetEntityAttributesMetadata(MasterToSlaveEntityMapping.MasterEntity.Identifier, ResultType.Details);
            Metadata slaveEntityAttributesDetails = GetEntityAttributesMetadata(MasterToSlaveEntityMapping.SlaveEntity.Identifier, ResultType.Details);

            ///create metadata objects used to prepare locally the attributes to either add, delete operationResult update
            Metadata metadata;

            ///create metadata objects used to prepare locally the attributes to either add, delete operationResult update
            List<MetadataAttribute>  attributesToCreate = new List<MetadataAttribute>();
            //List<MetadataAttribute> attributesToUpdate = new List<MetadataAttribute>();
            List<MetadataAttribute> attributesToDelete = new List<MetadataAttribute>();

            MetadataAttribute masterAttributeMetadata;
            MetadataAttribute slaveAttributeMetadata;
            
            ///iterate the list of master to slave attribute mappings in the metadata cache
            ///based on the attribute sync state we populate the 3 lists attributesToCreate, attributesToUpdate and attributesToDelete
            foreach (MasterToSlaveAttributeMapping masterToSlaveAttributeMapping in MasterToSlaveEntityMapping.MasterToSlaveAttributeMappings)
            {

                switch (masterToSlaveAttributeMapping.AttributeSyncState)
                {
                    case MDSAttributeSyncState.OnlyInSlave:

                        ///when the attribute exists only in the slave Entity, we need to delete it from the slave Entity
                        ///to do this we add the attribute to the delete list

                        InvokeInfoEvent(string.Format("Delete Attribute \"{0}\" from Entity \"{1}\" model \"{2}\" as it is no longer part of the master Entity \"{3}\" model \"{4}\"."
                                                , masterToSlaveAttributeMapping.SlaveAttribute.Name
                                                , MasterToSlaveEntityMapping.SlaveEntity.Name
                                                , MasterToSlaveEntityMapping.SlaveEntity.Model.Name
                                                , MasterToSlaveEntityMapping.MasterEntity.Name
                                                , MasterToSlaveEntityMapping.MasterEntity.Model.Name));

                        ///here we pick the relevant slave metadata details from the list using the Identifier (Guid) that exists in the metadata cache
                        slaveAttributeMetadata = slaveEntityAttributesDetails.Attributes.First(R=>R.Identifier.Id == masterToSlaveAttributeMapping.SlaveAttribute.Identifier.Id);
                        if(slaveAttributeMetadata == null)
                        {
                            throw new InvalidMetadataCacheException(string.Format("Cannot find the slave attribute \"{0}\" with the identifier \"{1}\" of Entity \"{2}\" in the local metadata cache. Cache may be out dated"
                                                                        , masterToSlaveAttributeMapping.SlaveAttribute.Name
                                                                        , masterToSlaveAttributeMapping.SlaveAttribute.Identifier.Id.ToString()
                                                                        , MasterToSlaveEntityMapping.SlaveEntity.Name)
                                                                        ,MasterToSlaveEntityMapping);
                        }
                        
		                attributesToDelete.Add(slaveAttributeMetadata);
                        
                        break;

                    case MDSAttributeSyncState.Different:

                        ///when the attribute is different we need to update the slave Entity based on the master Entity
                        ///to achieve this we perform both a delete and a create.
                        InvokeInfoEvent(string.Format("Update Attribute \"{0}\" in Entity \"{1}\" model \"{2}\" as it is different than the attribute in the master Entity \"{3}\" model \"{4}\"."
                                                , masterToSlaveAttributeMapping.SlaveAttribute.Name
                                                , MasterToSlaveEntityMapping.SlaveEntity.Name
                                                , MasterToSlaveEntityMapping.SlaveEntity.Model.Name
                                                , MasterToSlaveEntityMapping.MasterEntity.Name
                                                , MasterToSlaveEntityMapping.MasterEntity.Model.Name));

                        
                        ///pick the relevant slave metadata details from the list using the Identifier (Guid) from the metadata cache
                        slaveAttributeMetadata = slaveEntityAttributesDetails.Attributes.First(R => R.Identifier.Id == masterToSlaveAttributeMapping.SlaveAttribute.Identifier.Id);
                        if (slaveAttributeMetadata == null)
                        {
                            throw new InvalidMetadataCacheException(string.Format("Cannot find the slave attribute \"{0}\" with the identifier \"{1}\" of Entity \"{2}\" in the local metadata cache. Cache may be out dated"
                                                                        , masterToSlaveAttributeMapping.SlaveAttribute.Name
                                                                        , masterToSlaveAttributeMapping.SlaveAttribute.Identifier.Id.ToString()
                                                                        , MasterToSlaveEntityMapping.SlaveEntity.Name)
                                                                        , MasterToSlaveEntityMapping);
                        }

                        ///add metadata to the delete list
                        attributesToDelete.Add(slaveAttributeMetadata);

                        ///pick the relevant master metadata details from the list using the Identifier (Guid) from the metadata cache
                        masterAttributeMetadata = masterEntityAttributesDetails.Attributes.First(R=>R.Identifier.Id == masterToSlaveAttributeMapping.MasterAttribute.Identifier.Id);
                        if(masterAttributeMetadata == null)
                        {
                            throw new InvalidMetadataCacheException(string.Format("Cannot find the master attribute \"{0}\" with the identifier \"{1}\" of Entity \"{2}\" in the local metadata cache. Cache may be out dated"
                                                                        , masterToSlaveAttributeMapping.MasterAttribute.Name
                                                                        , masterToSlaveAttributeMapping.MasterAttribute.Identifier.Id.ToString()
                                                                        , MasterToSlaveEntityMapping.MasterEntity.Name)
                                                                        , MasterToSlaveEntityMapping);
                        }

                        ///add metadata to the create list
                        attributesToCreate.Add(CloneMetadataAttribute(masterAttributeMetadata, slaveEntity.Entities.First().Identifier, slaveModel.Identifier));
                        
                        break;

                    case MDSAttributeSyncState.OnlyInMaster:


                        ///when the attribute exists only in the master Entity, we need to add it to the slave Entity
                        ///to do this we add the attribute from the master Entity to the create list                        
                        InvokeInfoEvent(string.Format("Create new attribute \"{0}\" in Entity \"{1}\" model \"{2}\" as is added to the master entity \"{3}\" model \"{4}\"."
                                                , masterToSlaveAttributeMapping.SlaveAttribute.Name
                                                , MasterToSlaveEntityMapping.SlaveEntity.Name
                                                , MasterToSlaveEntityMapping.SlaveEntity.Model.Name
                                                , MasterToSlaveEntityMapping.MasterEntity.Name
                                                , MasterToSlaveEntityMapping.MasterEntity.Model.Name));

                        ///pick the relevant master metadata details from the list using the Identifier (Guid) from the metadata cache
                        masterAttributeMetadata = masterEntityAttributesDetails.Attributes.First(R=>R.Identifier.Id == masterToSlaveAttributeMapping.MasterAttribute.Identifier.Id);
                        if(masterAttributeMetadata == null)
                        {
                            throw new InvalidMetadataCacheException(string.Format("Cannot find the master attribute \"{0}\" with the identifier \"{1}\" of Entity \"{2}\" in the local metadata cache. Cache may be out dated"
                                                                        , masterToSlaveAttributeMapping.MasterAttribute.Name
                                                                        , masterToSlaveAttributeMapping.MasterAttribute.Identifier.Id.ToString()
                                                                        , MasterToSlaveEntityMapping.MasterEntity.Name)
                                                                        , MasterToSlaveEntityMapping);
                        }

                        ///add metadata to the create list
                        attributesToCreate.Add(CloneMetadataAttribute(masterAttributeMetadata, slaveEntity.Entities.First().Identifier, slaveModel.Identifier));
                        
                        break;

                }
            }

            ///now that we have the 2 list ready, we can use it to sync the metadata on the server
            ///to prevent duplicate errors we first delete and then create
            
            metadata = new Metadata();
            
            try 
	        {	        
		        if (attributesToDelete.Count > 0)
                    {
                        metadata.Attributes = attributesToDelete.ToArray();
                        operationResult = mdsServiceProxy.MetadataDelete(international, metadata);
                        ProcessResultForErrors(operationResult);
                    }

                    if (attributesToCreate.Count > 0)
                    {
                        metadata.Attributes = attributesToCreate.ToArray();
                        mdsServiceProxy.MetadataCreate(international, metadata, false, out operationResult);
                        ProcessResultForErrors(operationResult);
                    }
	        }
	        catch (Exception e)
	        {

                string message = string.Format("Failed to sync attributes for the mapping with the mapping code \"{0}\", of the Entity \"{1}\" between the master model \"{2}\" and the slave model \"{3}\"."
                                                , MasterToSlaveEntityMapping.MappingCode.ToString()
                                                , MasterToSlaveEntityMapping.SlaveEntity.Name
                                                , MasterToSlaveEntityMapping.MasterEntity.Model.Name
                                                , MasterToSlaveEntityMapping.SlaveEntity.Model.Name);

               InvokeErrorEvent(message, e);

		       throw new MDSException (message, e,MasterToSlaveEntityMapping);
	        }           
            
        }


        /// <summary>
        /// Enriches the metadata cache with the identifier of the model, entity and version for both the slave and master entity in the mapping object.
        /// The identifiers are used to work with the MDS web service.
        /// This method is here to minimize the requests to the web service
        /// </summary>
        /// <param name="masterToSlaveEntityMapping">The master to slave entity mapping.</param>
        private void EnrichMetadataCache(MasterToSlaveEntityMapping MasterToSlaveEntityMapping)
        {

            Model model;
            Entity entity;
            Iit.ITBI.MDSManager.Microsoft.MasterDataServices.Version version;

            model = GetModel(MasterToSlaveEntityMapping.MasterEntity.Model.Name);
            entity = GetEntity(model.Identifier, MasterToSlaveEntityMapping.MasterEntity.Name);
            version = GetVersion(model.Identifier);

            MasterToSlaveEntityMapping.MasterEntity.Identifier = entity.Identifier;
            MasterToSlaveEntityMapping.MasterEntity.Model.Identifier = model.Identifier;
            MasterToSlaveEntityMapping.MasterEntity.Model.VersionID = version.Identifier;

            model = GetModel(MasterToSlaveEntityMapping.SlaveEntity.Model.Name);
            entity = GetEntity(model.Identifier, MasterToSlaveEntityMapping.SlaveEntity.Name);
            version = GetVersion(model.Identifier);

            /// when the slave entity does not exits it will be null
            if (entity != null)
            {
                MasterToSlaveEntityMapping.SlaveEntity.Identifier = entity.Identifier;
            }
            MasterToSlaveEntityMapping.SlaveEntity.Model.Identifier = model.Identifier;
            MasterToSlaveEntityMapping.SlaveEntity.Model.VersionID = version.Identifier;

        }


        /// <summary>
        /// Clones the master attribute metadata into a new object, though using the slave model and slave Entity.
        /// By that we have a true copy of the master attribute, that can be committed to the slave Entity.
        /// </summary>
        /// <param name="masterAttributeMetadata">The master attribute metadata.</param>
        /// <param name="SlaveEntityIdentifier">The slave Entity identifier.</param>
        /// <param name="SlaveModelIdentifier">The slave model identifier.</param>
        /// <returns>A new object of type MetadataAttribute, that has all the properties of the master attribute with the with the identifiers of the slave Entity.</returns>
        private MetadataAttribute CloneMetadataAttribute(MetadataAttribute MasterAttributeMetadata, Identifier SlaveEntityIdentifier, Identifier SlaveModelIdentifier)
        {

            return new MetadataAttribute()
            {
                AttributeType = MasterAttributeMetadata.AttributeType,
                Identifier = new MemberTypeContextIdentifier()
                {
                    //entityId is coming from the target slave Entity                               
                    EntityId = SlaveEntityIdentifier,
                    //Id  is coming from the target slave Entity
                    Id = SlaveEntityIdentifier.Id,
                    //internal Id  is coming from the target slave Entity                             
                    InternalId = SlaveEntityIdentifier.InternalId,
                    ModelId = SlaveModelIdentifier,
                    MemberType = MasterAttributeMetadata.Identifier.MemberType,
                    Name = MasterAttributeMetadata.Identifier.Name
                },
                DataType = MasterAttributeMetadata.DataType,
                DataTypeInformation = MasterAttributeMetadata.DataTypeInformation,
                AuditInfo = MasterAttributeMetadata.AuditInfo,
                ChangeTrackingGroup = MasterAttributeMetadata.ChangeTrackingGroup,
                DisplayWidth = MasterAttributeMetadata.DisplayWidth,
                ExtensionData = MasterAttributeMetadata.ExtensionData,

                InputMaskId = MasterAttributeMetadata.InputMaskId,
                IsCode = MasterAttributeMetadata.IsCode,
                IsName = MasterAttributeMetadata.IsName,
                IsReadOnly = MasterAttributeMetadata.IsReadOnly,
                IsSystem = MasterAttributeMetadata.IsSystem,
                Permission = MasterAttributeMetadata.Permission,
                SortOrder = MasterAttributeMetadata.SortOrder

            };
        }


        /// <summary>
        /// Deletes the slave entity.
        /// </summary>
        /// <param name="MasterToSlaveEntityMapping">The master to slave entity mapping.</param>
        private void DeleteSlaveEntity(MasterToSlaveEntityMapping MasterToSlaveEntityMapping)
        {

            try
            { 
            
                
                Metadata entityMetadata = GetEntityMetadata(MasterToSlaveEntityMapping.SlaveEntity.Identifier);
                operationResult = mdsServiceProxy.MetadataDelete (international, entityMetadata);

                ProcessResultForErrors (operationResult);

            }
            catch (MDSException e)
            {
                if (e.MasterToSlaveEntityMapping == null)
                {
                    e.MasterToSlaveEntityMapping = MasterToSlaveEntityMapping;
                }

                if (!e.Data.Contains("CustomCallStack"))
                {
                    e.Data.Add("CustomCallStack", "at DeleteSlaveEntity()");
                }
                else
                {
                    e.Data["CustomCallStack"] = "at DeleteSlaveEntity()\r\n" + e.Data["CustomCallStack"].ToString();
                }

                throw;
            }
            catch (Exception e)
            {
                if (!e.Data.Contains("CustomCallStack"))
                {
                    e.Data.Add("CustomCallStack", "at DeleteSlaveEntity()");
                }
                else
                {
                    e.Data["CustomCallStack"] = "at DeleteSlaveEntity()\r\n" + e.Data["CustomCallStack"].ToString();
                }

                throw;
            }

        }


        /// <summary>
        /// Creates a new slave Entity based on the master Entity definition.
        /// The method will also create all the user defined attributes.
        /// The method will NOT sync the data of the Entity.
        /// </summary>
        /// <param name="masterToSlaveEntityMapping">The local metadata cache in for of a master to slave mapping object.</param>
        private void CreateNewSlaveEntity(MasterToSlaveEntityMapping MasterToSlaveEntityMapping)
        {

            
            
            //create MDS objects from mapping metadata
            Model masterModel = GetModel(MasterToSlaveEntityMapping.MasterEntity.Model.Name);
            Model slaveModel = GetModel(MasterToSlaveEntityMapping.SlaveEntity.Model.Name);
            Entity masterEntity = GetEntity(masterModel, MasterToSlaveEntityMapping.MasterEntity.Name);
                   
            //create masterAttributeMetadata new metadata instance      
            Metadata slaveEntityMetadata = new Metadata();
            slaveEntityMetadata.Entities = new Entity[1];

            //add masterAttributeMetadata new Entity in this new metadata, with values and fields from source Entity
            slaveEntityMetadata.Entities[0] = new Entity()
            {
                Identifier = new ModelContextIdentifier() { ModelId = slaveModel.Identifier, Name = MasterToSlaveEntityMapping.SlaveEntity.Name, Id = MasterToSlaveEntityMapping.SlaveEntity.MUID },
                AuditInfo = masterEntity.AuditInfo,
                ExtensionData = masterEntity.ExtensionData,
                ExplicitHierarchies = masterEntity.ExplicitHierarchies,
                IsBase = masterEntity.IsBase,
                IsFlat = masterEntity.IsFlat,
                IsSystem = masterEntity.IsSystem
            };

            ///creating the slave entity with system attributes only, and returning the newly created entity metadata to the same object
            ///this is done to get the identifiers of the slave entity that is created.
            //newlyCreatedEntityMetadata = mdsServiceProxy.MetadataCreate(international, entityMetadata, true, out operationResult);
            slaveEntityMetadata = mdsServiceProxy.MetadataCreate(international, slaveEntityMetadata, true, out operationResult);

            
            
            ///get master Entity attributes
            MetadataResultOptions masterEntityAttributeResultOptions = new MetadataResultOptions();
            MetadataSearchCriteria masterEntityAttributeSearchCriteria = new MetadataSearchCriteria();

            masterEntityAttributeResultOptions.Attributes = ResultType.Details;
            masterEntityAttributeSearchCriteria.SearchOption = SearchOption.UserDefinedObjectsOnly;
            masterEntityAttributeSearchCriteria.Entities = new Identifier[1] { masterEntity.Identifier };

            Metadata masterEntityMetadata;
            masterEntityMetadata = mdsServiceProxy.MetadataGet(international, masterEntityAttributeResultOptions, masterEntityAttributeSearchCriteria, out operationResult);

            //create masterAttributeMetadata new metadata instance for attributes to copy
            Metadata attributesToCreate = new Metadata();
            attributesToCreate.Attributes = new MetadataAttribute[masterEntityMetadata.Attributes.Length];

            int i = 0;

            //for each source Entity attribute, create masterAttributeMetadata new attribute containing values and fields from this source Entity
            foreach (MetadataAttribute masterEntityAttribute in masterEntityMetadata.Attributes)
            {
                MetadataAttribute ma = new MetadataAttribute()
                {
                    AttributeType = masterEntityAttribute.AttributeType,
                    Identifier = new MemberTypeContextIdentifier()
                    {
                        //entityId is coming from the target Entity                               
                        EntityId = slaveEntityMetadata.Entities.First().Identifier,
                        //Id  is coming from the target Entity
                        Id = slaveEntityMetadata.Entities.First().Identifier.Id,
                        //internal Id  is coming from the target Entity                             
                        InternalId = slaveEntityMetadata.Entities.First().Identifier.InternalId,
                        ModelId = slaveModel.Identifier,
                        MemberType = masterEntityAttribute.Identifier.MemberType,
                        Name = masterEntityAttribute.Identifier.Name
                    },
                    DataType = masterEntityAttribute.DataType,
                    DataTypeInformation = masterEntityAttribute.DataTypeInformation,
                    AuditInfo = masterEntityAttribute.AuditInfo,
                    ChangeTrackingGroup = masterEntityAttribute.ChangeTrackingGroup,
                    DisplayWidth = masterEntityAttribute.DisplayWidth,
                    ExtensionData = masterEntityAttribute.ExtensionData,

                    InputMaskId = masterEntityAttribute.InputMaskId,
                    IsCode = masterEntityAttribute.IsCode,
                    IsName = masterEntityAttribute.IsName,
                    IsReadOnly = masterEntityAttribute.IsReadOnly,
                    IsSystem = masterEntityAttribute.IsSystem,
                    Permission = masterEntityAttribute.Permission,
                    SortOrder = masterEntityAttribute.SortOrder

                };

                //add all destination attributes to the metadata
                attributesToCreate.Attributes[i++] = ma;

            }

            //create the attributes metadata in the slave entity
            mdsServiceProxy.MetadataCreate(international, attributesToCreate, true, out operationResult);

            ProcessResultForErrors(operationResult);
            
        }


        /// <summary>
        /// Performs a MetadataGet search request for a model against the web service, using the model name as the search criteria.
        /// </summary>
        /// <param name="ModelName">The name of the model to find.</param>
        /// <returns>A Model object.</returns>
        private Model GetModel(string ModelName)
        {

            MetadataResultOptions modelsResultOptions = new MetadataResultOptions();
            MetadataSearchCriteria modelsSearchCriteria = new MetadataSearchCriteria();
            OperationResult modelSearchResult;

            modelsResultOptions.Models = ResultType.Identifiers;
            modelsSearchCriteria.SearchOption = SearchOption.UserDefinedObjectsOnly;
            
            Metadata modelsMetadata = mdsServiceProxy.MetadataGet(international, modelsResultOptions, modelsSearchCriteria, out modelSearchResult);

            ProcessResultForErrors(modelSearchResult);

            foreach (var model in modelsMetadata.Models)
            {
                if (model.Identifier.Name == ModelName)
                    return model;
            }

            return null;
        
        }

        /// <summary>
        /// Performs a MetadataGet search request for an Entity against the web service, using the model and the Entity name as the search criteria.
        /// </summary>
        /// <param name="model">The model of the Entity to find.</param>
        /// <param name="masterToSlaveMappingEntityName">The name of the Entity to find.</param>
        /// <returns>An Entity object.</returns>
        private Entity GetEntity(Model model, string EntityName)
        {
            return GetEntity(model.Identifier, EntityName);
        }

        /// <summary>
        /// Performs a MetadataGet search request for an Entity against the web service.
        /// Using the model identifier and the Entity name as the search criteria.
        /// </summary>
        /// <param name="model">The model of the Entity to find.</param>
        /// <param name="masterToSlaveMappingEntityName">The name of the Entity to find.</param>
        /// <returns>An Entity object.</returns>
        private Entity GetEntity(Identifier modelID, string EntityName)
        {

            MetadataResultOptions entitiesResultOptions = new MetadataResultOptions();
            MetadataSearchCriteria entitiesSearchCriteria = new MetadataSearchCriteria();
            OperationResult entitiesSearchResult;

            Identifier[] i;
            i = new Identifier[1];

            entitiesResultOptions.Entities = ResultType.Identifiers;
            entitiesSearchCriteria.SearchOption = SearchOption.BothUserDefinedAndSystemObjects;

            Metadata entitiesMetadata = new Metadata();

            i.SetValue(modelID, 0);
            entitiesSearchCriteria.Models = i;

            entitiesMetadata = mdsServiceProxy.MetadataGet(international, entitiesResultOptions, entitiesSearchCriteria, out entitiesSearchResult);

            ProcessResultForErrors(entitiesSearchResult);

            foreach (var Entity in entitiesMetadata.Entities)
            {
                if (Entity.Identifier.Name == EntityName)
                    return Entity;
            }

            return null;

        }

        /// <summary>
        /// Performs a MetadataGet search request for a metadata object of an Entity, against the web service.
        /// Using the Entity identifier as the search criteria.
        /// </summary>
        /// <param name="EntityIdentifier">The Entity identifier.</param>
        /// <returns>A Metadata object holding metadata of a single Entity.</returns>
        private Metadata GetEntityMetadata(Identifier EntityIdentifier)
        {
            MetadataResultOptions entitiesResultOptions = new MetadataResultOptions();
            MetadataSearchCriteria entitiesSearchCriteria = new MetadataSearchCriteria();
            OperationResult entitiesSearchResult;

            Identifier[] i;
            i = new Identifier[1];

            entitiesResultOptions.Entities = ResultType.Details;
            entitiesSearchCriteria.SearchOption = SearchOption.BothUserDefinedAndSystemObjects;

            Metadata entitiesMetadata = new Metadata();

            i.SetValue(EntityIdentifier, 0);
            entitiesSearchCriteria.Entities = i;

            entitiesMetadata = mdsServiceProxy.MetadataGet(international, entitiesResultOptions, entitiesSearchCriteria, out entitiesSearchResult);

            ProcessResultForErrors(entitiesSearchResult);

            return entitiesMetadata;           

        }

        /// <summary>
        /// Performs a MetadataGet search request for a metadata object of an attribute, against the web service.
        /// Using the Entity identifier as the search criteria.
        /// </summary>
        /// <param name="EntityIdentifier">The Entity identifier.</param>
        /// <param name="ResultType">An Enum to set the result type of the search Choose between None, Details and Identifier.</param>
        /// <returns>A Metadata object holding metadata of a single attribute.</returns>
        private Metadata GetEntityAttributesMetadata(Identifier EntityIdentifier, ResultType ResultType)
        {

            MetadataResultOptions entitiesResultOptions = new MetadataResultOptions();
            MetadataSearchCriteria entitiesSearchCriteria = new MetadataSearchCriteria();
            OperationResult entitiesSearchResult;

            Identifier[] i;
            i = new Identifier[1];

            entitiesResultOptions.Attributes = ResultType;
            entitiesSearchCriteria.SearchOption = SearchOption.UserDefinedObjectsOnly;

            Metadata entityAttributesMetadata = new Metadata();

            i.SetValue(EntityIdentifier, 0);
            entitiesSearchCriteria.Entities = i;

            entityAttributesMetadata = mdsServiceProxy.MetadataGet(international, entitiesResultOptions, entitiesSearchCriteria, out entitiesSearchResult);

            ProcessResultForErrors(entitiesSearchResult);

            return entityAttributesMetadata;

        }

        /// <summary>
        /// Performs a MetadataGet search request for an Entity against the web service.
        /// Using the model identifier and the Entity name as the search criteria.
        /// </summary>
        /// <param name="model">The model of the Entity to find.</param>
        /// <param name="masterToSlaveMappingEntityName">The name of the Entity to find.</param>
        /// <returns>An Entity object.</returns>
        private Iit.ITBI.MDSManager.Microsoft.MasterDataServices.Version GetVersion(Identifier modelID)
        {

            MetadataResultOptions entitiesResultOptions = new MetadataResultOptions();
            MetadataSearchCriteria entitiesSearchCriteria = new MetadataSearchCriteria();
            OperationResult entitiesSearchResult;

            Identifier[] i;
            i = new Identifier[1];

            entitiesResultOptions.Versions = ResultType.Identifiers;
            entitiesSearchCriteria.SearchOption = SearchOption.BothUserDefinedAndSystemObjects;

            Metadata versionsMetadata = new Metadata();

            i.SetValue(modelID, 0);
            entitiesSearchCriteria.Models = i;

            versionsMetadata = mdsServiceProxy.MetadataGet(international, entitiesResultOptions, entitiesSearchCriteria, out entitiesSearchResult);

            ProcessResultForErrors(entitiesSearchResult);

            ///taking the first open version in the array.
            ///we do this as we do not work with versions in MDS, and therefore we do have a standard yet
            ///for flagging the current version.
            ///If there are no open versions, then we return null
            for (int j = 0; j < versionsMetadata.Versions.Length; j++)
            {
                if (versionsMetadata.Versions[j].VersionStatus == VersionStatus.Open)
                {
                    return versionsMetadata.Versions[j];
                }
            }

            return null;

        }

        /// <summary>
        /// Search the operationResult objects for errors, if errors are found the method will throw an exception.
        /// </summary>
        /// <param name="result">The operationResult of a metadata search request.</param>
        /// <exception cref="MDSMetadataCommandException"></exception>
        private void ProcessResultForErrors(OperationResult result)
        {

            StringBuilder sb = new StringBuilder();
            bool hasErrors = false;
            string Context = "";
            
            if (result.Errors.Length > 0)
            {
                hasErrors = true;
                foreach (Error e in result.Errors)
                {
                    if (e.Context != null)
                    {
                        Context = "Context:" + ((object)e.Context.Type).ToString();
                    }

                    sb.AppendLine("operationResult:Error: " + e.Code + ":" + e.Description + Context);
                }
            }

            if (hasErrors)
            {
                InvokeErrorEvent (sb.ToString());

                throw new MDSMetadataCommandException(sb.ToString());
            }
    
        }

        /// <summary>
        /// Search the validationProcessResult objects for validation problems, if validation issues are found the method will throw an exception.
        /// </summary>
        /// <param name="validationProcessResult">The validation process result.</param>
        /// <param name="validationIssues">An array of possible validation issues.</param>
        /// <exception cref="MDSMetadataCommandException"></exception>
        private void ProcessResultForErrors(OperationResult result, ValidationProcessResult validationProcessResult, ValidationIssue[] validationIssues)
        {
            StringBuilder sb = new StringBuilder();
            bool hasErrors = false;
            string Context = "";

            if (result.Errors.Length > 0)
            {
                hasErrors = true;
                foreach (Error e in result.Errors)
                {
                    if (e.Context != null)
                    {
                        Context = "Context:" + ((object)e.Context.Type).ToString();
                    }

                    sb.AppendLine("operationResult:Error: " + e.Code + ":" + e.Description + Context);
                }
            }
            
            if (validationProcessResult.MembersInvalidCount > 0)
            {
                throw new MemberValidationIssuesException("Some or all members failed to validate", validationProcessResult, validationIssues);                            
            }

            if (hasErrors)
            {
                InvokeErrorEvent(sb.ToString());

                throw new MDSMetadataCommandException(sb.ToString());
            }

        }

        /// <summary>
        /// Check if the master data services instance has the needed entity to store the master to slave entity mappings.
        /// If the entity cannot be found, it will add it, hence install the master to entity mapping entity.
        /// </summary>
        public void InstallMasterToEntityMappingEntity ()
        {
            Model metadataDefinitionsModel = GetModel("Metadata");

            string masterToSlaveMappingEntityName = Properties.Settings.Default.MasterEntityMappingEntityName;

            Entity masterToSlaveMappingEntity = GetEntity(metadataDefinitionsModel.Identifier, masterToSlaveMappingEntityName);

            if (masterToSlaveMappingEntity == null)
            {
                
                //create masterAttributeMetadata new metadata instance      
                Metadata entityMetadata = new Metadata();
                entityMetadata.Entities = new Entity[1];

                //add masterAttributeMetadata new Entity in this new metadata, with values and fields from source Entity
                entityMetadata.Entities[0] = new Entity()
                {
                    Identifier = new ModelContextIdentifier() 
                        { 
                            ModelId = metadataDefinitionsModel.Identifier,
                            Name = masterToSlaveMappingEntityName,
                            Id = new Guid()
                        },
                    IsBase = false,
                    IsFlat = true,
                    IsSystem = false
                };

                ///creating the MasterToSlaveMapping entity with system attributes only, and returning the newly created entity metadata to the same object
                ///this is done to get the identifiers of the entity that is created.
                entityMetadata = mdsServiceProxy.MetadataCreate(international, entityMetadata, true, out operationResult);

                Metadata masterEntityMetadata;

                ///get master Entity attributes
                MetadataResultOptions masterEntityAttributeResultOptions = new MetadataResultOptions();
                MetadataSearchCriteria masterEntityAttributeSearchCriteria = new MetadataSearchCriteria();

                masterEntityAttributeResultOptions.Attributes = ResultType.Details;
                masterEntityAttributeSearchCriteria.SearchOption = SearchOption.UserDefinedObjectsOnly;
                masterEntityAttributeSearchCriteria.Entities = new Identifier[1] { GetEntity(metadataDefinitionsModel.Identifier, "MasterToSlaveMapping").Identifier };

                //Metadata masterEntityMetadata;
                masterEntityMetadata = mdsServiceProxy.MetadataGet(international, masterEntityAttributeResultOptions, masterEntityAttributeSearchCriteria, out operationResult);

                //create masterAttributeMetadata new metadata instance for attributes to copy
                Metadata attributesToCreate = new Metadata();
                attributesToCreate.Attributes = new MetadataAttribute[2];

                attributesToCreate.Attributes[0] = GetMasterEntityAttribute(metadataDefinitionsModel, entityMetadata.Entities.First());
                attributesToCreate.Attributes[1] = GetSlaveModelAttribute(metadataDefinitionsModel, entityMetadata.Entities.First());

                //create the attributes 
                mdsServiceProxy.MetadataCreate(international, attributesToCreate, true, out operationResult);

                ProcessResultForErrors(operationResult);

            }
        
        }



        private MetadataAttribute GetMasterEntityAttribute(Model MetadataDefinitionsModel, Entity MappingEntity)
        {

            //get the  "Entity Metadata Definition" entity and use is as the domain entity for the attribute
            Entity domainEntity = GetEntity(MetadataDefinitionsModel.Identifier, "Entity Metadata Definition");

            //prepare the attributes
            MetadataAttribute ma = new MetadataAttribute()
            {
                AttributeType = AttributeType.Domain,
                Identifier = new MemberTypeContextIdentifier()
                {
                    EntityId = MappingEntity.Identifier,
                    Id = MappingEntity.Identifier.Id,
                    InternalId = MappingEntity.Identifier.InternalId,
                    ModelId = MetadataDefinitionsModel.Identifier,
                    MemberType = MemberType.Leaf,
                    Name = "Master Entity"
                },
                FullyQualifiedName = "Metadata:" + MappingEntity.Identifier.Name + ":Leaf:Master Entity",
                DomainEntityId = domainEntity.Identifier,
                DomainEntityIsFlat = true,

                DataType = AttributeDataType.Text,
                DataTypeInformation = 100,
                //AuditInfo = masterEntityAttribute.AuditInfo,
                ChangeTrackingGroup = 0,
                DisplayWidth = 100,
                //ExtensionData = masterEntityAttribute.ExtensionData,

                //InputMaskId = masterEntityAttribute.InputMaskId,
                IsCode = false,
                IsName = false,
                IsReadOnly = false,
                IsSystem = false,
                //Permission = masterEntityAttribute.Permission,
                SortOrder = 18

            };

            return ma;
        
        }


        private MetadataAttribute GetSlaveModelAttribute(Model MetadataDefinitionsModel, Entity MappingEntity)
        {

            //get the "Model Metadata Definition" entity and use is as the domain entity for the attribute
            Entity domainEntity = GetEntity(MetadataDefinitionsModel.Identifier, "Model Metadata Definition");

            //prepare the attributes
            MetadataAttribute ma = new MetadataAttribute()
            {
                AttributeType = AttributeType.Domain,
                Identifier = new MemberTypeContextIdentifier()
                {
                    EntityId = MappingEntity.Identifier,
                    Id = MappingEntity.Identifier.Id,
                    InternalId = MappingEntity.Identifier.InternalId,
                    ModelId = MetadataDefinitionsModel.Identifier,
                    MemberType = MemberType.Leaf,
                    Name = "Slave Model"
                },

                FullyQualifiedName = "Metadata:" + MappingEntity.Identifier.Name + ":Leaf:Slave Model",
                DomainEntityId = domainEntity.Identifier,
                DomainEntityIsFlat = true,

                DataType = AttributeDataType.Text,
                DataTypeInformation = 100,
                //AuditInfo = masterEntityAttribute.AuditInfo,
                ChangeTrackingGroup = 0,
                DisplayWidth = 100,
                //ExtensionData = masterEntityAttribute.ExtensionData,

                //InputMaskId = masterEntityAttribute.InputMaskId,
                IsCode = false,
                IsName = false,
                IsReadOnly = false,
                IsSystem = false,
                //Permission = masterEntityAttribute.Permission,
                SortOrder = 18

            };

            return ma;

        }





        #endregion

        #region Events

        public event EventHandler<MessageEventArgs> OnDebug;
        public event EventHandler<MessageEventArgs> OnInfo;
        public event EventHandler<MessageEventArgs> OnWarning;
        public event EventHandler<MessageEventArgs> OnError;


        protected virtual void InvokeDebugEvent(MessageEventArgs e)
        {
            if (this.OnDebug != null)
                this.OnDebug.Invoke(this, e);
        }
        protected virtual void InvokeDebugEvent(string Message, Exception e)
        {
            MessageEventArgs messageEventArgs = new MessageEventArgs();

            messageEventArgs.Message = Message;
            messageEventArgs.e = e;

            InvokeDebugEvent(messageEventArgs);
        }
        protected virtual void InvokeDebugEvent(string Message)
        {
            MessageEventArgs messageEventArgs = new MessageEventArgs();

            messageEventArgs.Message = Message;

            InvokeDebugEvent(messageEventArgs);
        }

        protected virtual void InvokeInfoEvent(MessageEventArgs e)
        {
            if (this.OnInfo != null)
                this.OnInfo.Invoke(this, e);
        }
        protected virtual void InvokeInfoEvent(string Message, Exception e)
        {
            MessageEventArgs messageEventArgs = new MessageEventArgs();
            
            messageEventArgs.Message = Message;
            messageEventArgs.e = e;

            InvokeInfoEvent(messageEventArgs);
        }
        protected virtual void InvokeInfoEvent(string Message)
        {
            MessageEventArgs messageEventArgs = new MessageEventArgs();

            messageEventArgs.Message = Message;
         
            InvokeInfoEvent(messageEventArgs);
        }

        protected virtual void InvokeWarningEvent(MessageEventArgs e)
        {
            if (this.OnWarning != null)
                this.OnWarning.Invoke(this, e);
        }
        protected virtual void InvokeWarningEvent(string Message, Exception e)
        {
            MessageEventArgs messageEventArgs = new MessageEventArgs();

            messageEventArgs.Message = Message;
            messageEventArgs.e = e;

            InvokeWarningEvent(messageEventArgs);
        }
        protected virtual void InvokeWarningEvent(string Message)
        {
            MessageEventArgs messageEventArgs = new MessageEventArgs();

            messageEventArgs.Message = Message;

            InvokeWarningEvent(messageEventArgs);
        }

        protected virtual void InvokeErrorEvent(MessageEventArgs e)
        {
            if (this.OnError != null)
                this.OnError.Invoke(this, e);
        }
        protected virtual void InvokeErrorEvent(string Message)
        {
            MessageEventArgs messageEventArgs = new MessageEventArgs();

            messageEventArgs.Message = Message;

            InvokeErrorEvent(messageEventArgs);
        }
        protected virtual void InvokeErrorEvent(string Message, Exception e)
        {
            MessageEventArgs messageEventArgs = new MessageEventArgs();

            messageEventArgs.Message = Message;
            messageEventArgs.e = e;

            InvokeErrorEvent(messageEventArgs);
        }

        #endregion

    }
}
